package org.erikaredmark.monkeyshines.editor; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.awt.image.BufferedImage; import java.io.IOException; import java.nio.file.Path; import java.util.Collection; import java.util.List; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.Timer; import org.erikaredmark.monkeyshines.GameConstants; import org.erikaredmark.monkeyshines.Goodie; import org.erikaredmark.monkeyshines.ImmutablePoint2D; import org.erikaredmark.monkeyshines.Point2D; import org.erikaredmark.monkeyshines.Sprite; import org.erikaredmark.monkeyshines.TileMap; import org.erikaredmark.monkeyshines.World; import org.erikaredmark.monkeyshines.World.GoodieLocationPair; import org.erikaredmark.monkeyshines.background.Background; import org.erikaredmark.monkeyshines.editor.MapEditor.TileBrush; import org.erikaredmark.monkeyshines.editor.dialog.AuthorshipDialog; import org.erikaredmark.monkeyshines.editor.dialog.EditHazardsDialog; import org.erikaredmark.monkeyshines.editor.dialog.EditHazardsModel; import org.erikaredmark.monkeyshines.editor.dialog.SetBackgroundDialog; import org.erikaredmark.monkeyshines.editor.dialog.SpriteChooserDialog; import org.erikaredmark.monkeyshines.editor.dialog.SpritePropertiesDialog; import org.erikaredmark.monkeyshines.editor.dialog.SpritePropertiesModel; import org.erikaredmark.monkeyshines.editor.model.Template; import org.erikaredmark.monkeyshines.editor.model.TemplateUtils; import org.erikaredmark.monkeyshines.encoder.EncodedWorld; import org.erikaredmark.monkeyshines.encoder.WorldIO; import org.erikaredmark.monkeyshines.encoder.exception.WorldSaveException; import org.erikaredmark.monkeyshines.resource.CoreResource; import org.erikaredmark.monkeyshines.resource.WorldResource; import com.google.common.base.Function; import com.google.common.base.Optional; /** * * This window runs the same game engine that the game runs, and allows the user to edit the world. * <p/> * Brush types are set from {@code BrushPalette} window. * * @author Erika Redmark * */ @SuppressWarnings("serial") public final class LevelDrawingCanvas extends JPanel implements MouseListener, MouseMotionListener, KeyListener { /** * * Creates the canvas, is designed to display and allow editing of worlds. * * @param keys * key input to allow the canvas to respond to keyboard shortcuts * * @param worldLoaded * a callback that is called when a new world is loaded, indicating the resource for that world * for other functions at require it in the main editor * */ public LevelDrawingCanvas(final Function<World, Void> worldLoaded) { super(); this.setLayout(null); this.worldLoaded = worldLoaded; currentGoodieType = Goodie.Type.BANANA; // Need to pick something for default. Bananas are good. currentState = EditorState.NO_WORLD_LOADED; setMinimumSize( new Dimension(GameConstants.SCREEN_WIDTH, GameConstants.SCREEN_HEIGHT) ); setPreferredSize( new Dimension(GameConstants.SCREEN_WIDTH, GameConstants.SCREEN_HEIGHT) ); setVisible(true); addMouseMotionListener(this); addMouseListener(this); setFocusable(true); addKeyListener(this); // Optimisations setDoubleBuffered(true); mousePosition = Point2D.of(0, 0); editorFakeGameTimer = new Timer( GameConstants.EDITOR_SPEED, new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { if (currentState == EditorState.NO_WORLD_LOADED) return; repaint(); } }); editorFakeGameTimer.start(); } /** * * Decodes a living instance of the encoded world into the editor. If a world instance already existed, * the {@code WorldResource} object associated is disposed first * * @param world * the encoded world to load into the editor and start editing * */ public void loadWorld(final EncodedWorld world, final WorldResource rsrc) { if (currentWorldEditor != null) { currentWorldEditor.getWorldResource().dispose(); } currentWorldEditor = WorldEditor.fromEncoded(world, rsrc); setCurrentScreenEditor(currentWorldEditor.getLevelScreenEditor(1000) ); changeState(EditorState.USE_MAP_EDITOR); // Map editor initialised in setting screen editor. currentMapEditor.setBrushAndId(TileBrush.SOLIDS, 0); worldLoaded.apply(currentWorldEditor.getWorld() ); } // Sets the current screen editor, removing if needed the current map editor for the screen and adding a new one // to the existing panel. private void setCurrentScreenEditor(LevelScreenEditor screenEditor) { currentScreenEditor = screenEditor; if (currentMapEditor != null) { this.remove(currentMapEditor); } // Reset the map editor to the new screen, setting the default paintbrush as well. Note that this could be the // first time being set, in which case there was no previous screen. final MapEditor previous = currentMapEditor; currentMapEditor = new MapEditor(screenEditor.getLevelScreen().getMap(), screenEditor.getBackground(), currentWorldEditor.getWorld(), false); currentMapEditor.setLocation(0, 0); if (previous != null) { currentMapEditor.setBrushAndId(previous.getCurrentBrush(), previous.getTileId() ); } this.add(currentMapEditor); } /** * * Saves the current state of the world in the editor to the given file * * @param location * the location to save to * * @throws WorldSaveException * if an error is occurred saving this editors state to the given file due to * the world being in a corrupt state * * @throws IOException * if a low level I/O error prevents saving the world * */ public void saveWorld(final Path location) throws WorldSaveException, IOException { WorldIO.saveOnlyWorld(this.currentWorldEditor, location); } /** * * Returns the current world editor for the level editor. This call will fail if called when no * world is loaded. * * @return * world editor, never {@code null} * * @throws IllegalStateException * if no world is loaded in the editor * */ public WorldEditor getWorldEditor() { if (this.currentState == EditorState.NO_WORLD_LOADED) throw new IllegalStateException("No world loaded"); return this.currentWorldEditor; } /** * * Returns the current state of this object. * */ public EditorState getState() { return this.currentState; } /** * * Determines if the screen pointed to by the id exists * * @param id * id of screen to check for * * @return * {@code true} if the screen exists by that id, {@code false} if otherwise * * @throws IllegalStateException * if no world is loaded in the editor * */ public boolean screenExists(int id) { if (this.currentState == EditorState.NO_WORLD_LOADED) throw new IllegalArgumentException("No world loaded"); return this.currentWorldEditor.screenExists(id); } /** * Resolves the location the person clicked and places the selected Goodie on * the map. * @param x * @param y */ private void addGoodie(final int x, final int y) { if (this.currentState == EditorState.NO_WORLD_LOADED) return; currentWorldEditor.addGoodie(currentScreenEditor.getId(), x / GameConstants.GOODIE_SIZE_X, y / GameConstants.GOODIE_SIZE_Y, currentGoodieType ); } private void eraseGoodieAt(final int mouseX, final int mouseY) { if (this.currentState == EditorState.NO_WORLD_LOADED) return; int row = mouseX / GameConstants.TILE_SIZE_X; int col = mouseY / GameConstants.TILE_SIZE_Y; currentWorldEditor.removeGoodie(currentScreenEditor.getId(), row, col); } /** * * Sets bonzos starting location on the currently loaded screen. This method accepts pixel mouse coordinates, and will * automatically snap them to the right tile. The tile clicked becomes the upper-left tile of bonzos 2 by 2 character. * * @param x * x location, in pixels * * @param y * y location, in pixels */ public void setBonzo(final int x, final int y) { currentWorldEditor.setBonzo(snapMouseX(x) / GameConstants.TILE_SIZE_X, snapMouseY(y) / GameConstants.TILE_SIZE_Y, currentScreenEditor.getId() ); } // Snap the mouse cursor to the square. For example, 125 and 135 should all go down to 120 public int snapMouseX(final int X) { int takeAwayX = X % GameConstants.TILE_SIZE_X; return X - takeAwayX; } public int snapMouseY(final int Y) { int takeAwayY = Y % GameConstants.TILE_SIZE_Y; return Y - takeAwayY; } public void actionEditOffscreenSprites() { List<Sprite> sprites = currentScreenEditor.getSpritesOutOfBounds(); switch(sprites.size() ) { case 0: JOptionPane.showMessageDialog(this, "There are no offscreen sprites to edit"); break; case 1: { Sprite s = sprites.get(0); SpritePropertiesModel model = SpritePropertiesDialog.launch(this, this.currentWorldEditor.getWorldResource(), s); changeSpriteFromDialogModel(s, model); break; } default: { Optional<Sprite> sOp = SpriteChooserDialog.launch(this, sprites, this.currentWorldEditor.getWorldResource() ); if (sOp.isPresent() ) { Sprite s = sOp.get(); SpritePropertiesModel model = SpritePropertiesDialog.launch(this, this.currentWorldEditor.getWorldResource(), s); changeSpriteFromDialogModel(s, model); } } } } public void actionPlaceBonzo() { if (this.currentState == EditorState.NO_WORLD_LOADED) return; changeState(EditorState.PLACING_BONZO); } public void actionResetScreen() { if (this.currentState == EditorState.NO_WORLD_LOADED) return; reloadCurrentScreen(); } /** * * Blocks and opens a window for the level editor that allows editing of hazards in the map. * */ public void openEditHazards() { EditHazardsModel model = EditHazardsDialog.launch(this, currentWorldEditor.getWorldResource(), currentWorldEditor.getHazards() ); // Sync any changes back to save state currentWorldEditor.setHazards(model.getHazards() ); } /** * * Loads up a background picker for the world. Whatever the user's selection is, the background will be set * to that. * */ public void actionChangeBackground() { Background newBackground = SetBackgroundDialog.launch(this.currentWorldEditor.getWorldResource(), this.currentScreenEditor.getBackground() ); currentScreenEditor.setBackground(newBackground); // update map editor too, as it does not use the same reference currentMapEditor.changeBackground(newBackground); } /** * * Gives the user a text field to enter the name of the author or authors of the world, or technically speaking whatever they want. * If the dialog is cancelled the author is unchanged. * */ public void actionSetAuthor() { final String newAuthor = AuthorshipDialog.launch(currentWorldEditor.getWorld().getAuthor() ); currentWorldEditor.getWorld().setAuthor(newAuthor); } /** * * Changes the internal state of the editor from one state to the other, making any additional updates as needed. * * @param newState * */ private void changeState(EditorState newState) { // Make no state changes if the state isn't actually changing. Still update the tile indicator: no state change can // still mean the type of tile has changed, requiring redraw if (currentState == newState) { updateTileIndicator(); return; } if ( newState == EditorState.EDITING_SPRITES || newState == EditorState.DELETING_SPRITES) { // Set a condition for the game timer to stop animating sprites. // Stopping the timer completely would look like a freeze, so we don't do that. currentScreenEditor.stopAnimatingSprites(); } else { // Transitioning out of editing sprites state always restores the sprite animation. currentScreenEditor.startAnimatingSprites(); } currentState = newState; // Finally, check our state. if we are ceding control to the map editor, no worries, the correct paintbrush // info will be set elsewhere. otherwise, we must set the map editor to a state where it knows it is not being // used, even if it doesn't get the clicks so that the tile indicator doesn't ghost. if (currentState != EditorState.USE_MAP_EDITOR) { currentMapEditor.setBrushAndId(TileBrush.NONE, 0); updateTileIndicator(); } } private void updateTileIndicator() { if (currentState == EditorState.PLACING_GOODIES) { BufferedImage goodieSheet = currentWorldEditor.getWorldResource().getGoodieSheet(); int srcX = currentGoodieType.getDrawX(); int srcY = currentGoodieType.getDrawY(); indicatorImage = new BufferedImage(GameConstants.TILE_SIZE_X, GameConstants.TILE_SIZE_Y, goodieSheet.getType() ); Graphics2D g = indicatorImage.createGraphics(); try { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f) ); g.drawImage(goodieSheet, 0, 0, GameConstants.TILE_SIZE_X, GameConstants.TILE_SIZE_Y, srcX, srcY, srcX + GameConstants.TILE_SIZE_X, srcY + GameConstants.TILE_SIZE_Y, null); } finally { g.dispose(); } } else if (currentState == EditorState.PLACING_TEMPLATES) { // draw the tilemap from the template onto a separate buffer with 50% transparency. This is a bit of a complicated indicator... assert currentTemplate != null : "Null template during updating indicator"; BufferedImage templateRendered = TemplateUtils.renderTemplate(currentTemplate, currentWorldEditor.getWorldResource() ); indicatorImage = new BufferedImage(templateRendered.getWidth(), templateRendered.getHeight(), templateRendered.getType() ); Graphics2D g = indicatorImage.createGraphics(); try { g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f) ); g.drawImage(templateRendered, 0, 0, null); } finally { g.dispose(); } } else { indicatorImage = null; } } public void actionChangeScreen(Integer screenId) { if (this.currentState == EditorState.NO_WORLD_LOADED) return; boolean wasAnimating = currentScreenEditor.isAnimatingSprites(); LevelScreenEditor screenEditor = currentWorldEditor.getLevelScreenEditor(screenId); setCurrentScreenEditor(screenEditor); currentWorldEditor.changeCurrentScreen(screenEditor); // Make sure sprites are not animating if they weren't before, and animating if they // were if (wasAnimating) screenEditor.startAnimatingSprites(); else screenEditor.stopAnimatingSprites(); } /** * * Reloads the current screen, resyncing the modified sprites and all animations together to how it will look when * bonzo enters the screen in-game. In-between states that are possible in the editor are not saved; a screen after reset * is how it will appear when bonzo enters it. Useful when aligning sprites that need to work with each other. * */ public void reloadCurrentScreen() { currentScreenEditor.resetCurrentScreen(); } /** * * Sets whether the coordinates displayed in the editor should be invisible (false) or visible * (true) * * @param visible * */ public void setDisplayingCoordinates(boolean visible) { this.drawingCoordinates = visible; } /** * * Toggles display of coordinates. if displayed, they will become invisible, and vice-versa * */ public void toggleDisplayingCoordinates() { setDisplayingCoordinates(!drawingCoordinates); } /** * * Gets the sprite at the given location, bringing up a popup dialog to select between multiple if there are multiple * in the given location. * * @param location * the location to resolve the sprites * * @return * a single sprite at the given location, or no sprite it none such exist * */ public Optional<Sprite> resolveSpriteAtLocation(ImmutablePoint2D location) { List<Sprite> sprites = currentScreenEditor.getSpritesWithin(location, 3); switch(sprites.size() ) { case 0: return Optional.absent(); case 1: return Optional.of(sprites.get(0) ); // Most complex, requires dialog. default: return SpriteChooserDialog.launch(this, sprites, this.currentWorldEditor.getWorldResource() ); } } /** * * Sets the brush tile type to the given type and sets the id of the brush to the passed id. * <p/> * This will automatically deduce the actual tile from the passed paintbrush. * Calling this method will change the state of the editor to PLACING mode for either tiles, * hazards, or goodies, based on the paintbrush type. * <p/> * Since sprites are basically the same thing (an id for initial placement) this method can also handle * the sprite brush and will default the creation of the sprite to the id presented. * <p/> * This method can NOT handle {@code PaintbrushType.TEMPLATE}. Use {@code setTemplateBrush} instead. * <p/> * If there is an open template editor, that editor will also have their brush set if the brush is a * compatible type with plain map editing (as in no sprites or goodies) * * @param type * the paintbrush type * * @param id * the id of the tile * */ public void setTileBrushAndId(PaintbrushType type, int id) { switch(type) { case SOLIDS: // break omitted case THRUS: // break omitted case SCENES: // break omitted case CONVEYERS_CLOCKWISE: // break omitted case CONVEYERS_ANTI_CLOCKWISE: // break omitted case COLLAPSIBLE: // break omitted case HAZARDS: // break omitted case ERASER_TILES: changeState(EditorState.USE_MAP_EDITOR); currentMapEditor.setBrushAndId(MapEditor.paintbrushToTilebrush(type), id); break; case GOODIES: currentGoodieType = Goodie.Type.byValue(id); changeState(EditorState.PLACING_GOODIES); break; case PLACE_SPRITES: currentSpriteId = id; changeState(EditorState.PLACING_SPRITES); break; case EDIT_SPRITES: currentSpriteId = id; changeState(EditorState.EDITING_SPRITES); break; case ERASER_GOODIES: changeState(EditorState.ERASING_GOODIES); break; case ERASER_SPRITES: changeState(EditorState.DELETING_SPRITES); break; case TEMPLATE: throw new RuntimeException("Templates must be set explicitly via 'setTemplateBrush'"); default: throw new RuntimeException("method not updated to handle new brush type " + type); } } /** * * Sets the current brush to be a template brush, using the given template as the template to be drawn. * * @param template */ public void setTemplateBrush(Template template) { currentTemplate = template; changeState(EditorState.PLACING_TEMPLATES); } @Override public void mouseClicked(MouseEvent e) { mousePosition.setX(e.getX() ); mousePosition.setY(e.getY() ); currentState.defaultClickAction(this); } @Override public void mouseEntered(MouseEvent e) { } @Override public void mouseExited(MouseEvent e) { } @Override public void mouseReleased(MouseEvent e) { } @Override public void mousePressed(MouseEvent e) { } @Override public void mouseDragged(MouseEvent e) { mousePosition.setX(e.getX() ); mousePosition.setY(e.getY() ); currentState.defaultDragAction(this); } @Override public void mouseMoved(MouseEvent e) { // We update map editor and this position mousePosition.setX(e.getX() ); mousePosition.setY(e.getY() ); if (currentMapEditor != null) { currentMapEditor.mouseMoved(mousePosition.x(), mousePosition.y() ); } } @Override public void keyPressed(KeyEvent key) { } @Override public void keyReleased(KeyEvent key) { } @Override public void keyTyped(KeyEvent key) { switch (key.getKeyChar() ) { case 'c': toggleDisplayingCoordinates(); break; } } /** * * Note: Each call to paint is synced with updating game state, so this updates all relevant states before painting them. * */ @Override public void paint(Graphics g) { super.paint(g); Graphics2D g2d = (Graphics2D) g; /* If the current world is null, there is no data to load. Draw a white screen*/ if (currentState == EditorState.NO_WORLD_LOADED) { g2d.setColor(Color.WHITE); g2d.fillRect(0, 0, GameConstants.WINDOW_WIDTH, GameConstants.WINDOW_HEIGHT); } else { currentMapEditor.update(); currentMapEditor.paint(g2d); // Map Editor has no concept of goodies or sprites, so paint them separately. List<Sprite> sprites = currentScreenEditor.getSpritesOnScreen(); for (Sprite s : sprites) { if (currentScreenEditor.isAnimatingSprites() ) s.update(); s.paint(g2d); } Collection<GoodieLocationPair> goodies = currentWorldEditor.getWorld().getGoodiesForLevel(currentScreenEditor.getId() ); for (GoodieLocationPair good : goodies) { good.goodie.update(); good.goodie.paint(g2d); } // BELOW: Additional overlays that are not part of the actual world // Draw bonzo starting location of screen final BufferedImage bonz = CoreResource.INSTANCE.getTransparentBonzo(); final ImmutablePoint2D start = this.currentScreenEditor.getBonzoStartingLocationPixels(); g2d.drawImage(bonz, start.x(), start.y(), start.x() + bonz.getWidth(), start.y() + bonz.getHeight(), 0, 0, bonz.getWidth(), bonz.getHeight(), null); int snapX = EditorMouseUtils.snapMouseX(mousePosition.x() ); int snapY = EditorMouseUtils.snapMouseY(mousePosition.y() ); // Draw indicator for mouse position only if map editor hasn't already taken care of it drawTileIndicator(g2d, snapX, snapY); // Finally, update coordinate data if (drawingCoordinates) { drawCoordinateInfo(g2d, snapX, snapY); } } } private void drawTileIndicator(Graphics2D g2d, int snapX, int snapY) { if (currentState == EditorState.USE_MAP_EDITOR) return; if (indicatorImage == null) { g2d.setColor(Color.green); g2d.drawRect(snapX, snapY, GameConstants.TILE_SIZE_X, GameConstants.TILE_SIZE_Y); } else { g2d.drawImage(indicatorImage, snapX, snapY, snapX + indicatorImage.getWidth(), snapY + indicatorImage.getHeight(), 0, 0, indicatorImage.getWidth(), indicatorImage.getHeight(), null); } } /** * * Draws a string indicating the x/y location of the tile at the lower-left corner of the screen. * * @param g2d * */ private void drawCoordinateInfo(Graphics2D g2d, int snapX, int snapY) { int tileX = snapX / GameConstants.TILE_SIZE_X; int tileY = snapY / GameConstants.TILE_SIZE_Y; String location = tileX + ", " + tileY; // white box to bring visibility int boxWidth = g2d.getFontMetrics().stringWidth(location) + 4; g2d.setColor(Color.WHITE); g2d.fillRect(2, GameConstants.SCREEN_HEIGHT - 24, boxWidth, 14); g2d.setColor(Color.BLACK); g2d.drawString(location, 4, GameConstants.SCREEN_HEIGHT - 12); } /** * * Interface implemented only by EditorState. Intended to allow action information to be included in the state object itself * to prevent instanceof bugs. * <p/> * Mouse location and all other editor properties must be set before calling the state methods. * */ private interface EditorStateAction { /** Action for the editor in this state during a mouse click */ public void defaultClickAction(LevelDrawingCanvas editor); /** Action for the editor in this state during a mouse drag */ public void defaultDragAction(LevelDrawingCanvas editor); } /** * * Represents the current state of the editor, like what is being placed. Note: If no world is loaded, many functions will * not work, and state change will not be possible until a world is loaded. * <p/> * No states need check for nulls because client code will make sure state changes only occur when allowed * * @author Erika Redmark * */ public enum EditorState implements EditorStateAction { // The most common state; map editor is also listening to mouse clicks and taking care of updates. We delegate to the map // editor here. USE_MAP_EDITOR { @Override public void defaultClickAction(LevelDrawingCanvas editor) { editor.currentMapEditor.mouseClicked(editor.mousePosition.x(), editor.mousePosition.y() ); } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, PLACING_GOODIES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { editor.addGoodie(editor.mousePosition.x(), editor.mousePosition.y() ); } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, ERASING_GOODIES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { editor.eraseGoodieAt(editor.mousePosition.x(), editor.mousePosition.y() ); } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, PLACING_TEMPLATES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { assert editor.currentTemplate != null : "Null template during drawing"; TileMap map = editor.currentScreenEditor.getLevelScreen().getMap(); // TODO currently no support for offsets; always draws at top-left. editor.currentTemplate.drawTo(map, editor.mousePosition.y() / GameConstants.TILE_SIZE_Y, editor.mousePosition.x() / GameConstants.TILE_SIZE_X, 0, 0); } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, PLACING_SPRITES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { SpritePropertiesModel model = SpritePropertiesDialog.launch( editor, editor.currentWorldEditor.getWorldResource(), editor.currentSpriteId, ImmutablePoint2D.of(editor.mousePosition.x(), editor.mousePosition.y() ) ); if (model.isOkay() ) { editor.currentScreenEditor.addSprite(model.getSpriteId(), model.getSpriteStartingLocation(), model.getSpriteBoundingBox(), model.getSpriteVelocity(), model.getAnimationType(), model.getAnimationSpeed(), model.getSpriteType(), model.getForceDirection(), model.getTwoWayFacing(), editor.currentWorldEditor.getWorldResource() ); } } @Override public void defaultDragAction(LevelDrawingCanvas editor) { /* No Drag Action */ } }, EDITING_SPRITES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { Optional<Sprite> selected = editor.resolveSpriteAtLocation(ImmutablePoint2D.from(editor.mousePosition) ); if (selected.isPresent() ) { // Open properties editor for sprite SpritePropertiesModel model = SpritePropertiesDialog.launch(editor, editor.currentWorldEditor.getWorldResource(), selected.get() ); // Remove sprite from screen, create a new one with new properties. editor.changeSpriteFromDialogModel(selected.get(), model); } } // Still have to forward... slight drag movement would otherwise be ignored. @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, DELETING_SPRITES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { Optional<Sprite> selected = editor.resolveSpriteAtLocation(ImmutablePoint2D.from(editor.mousePosition) ); if (selected.isPresent() ) { editor.currentScreenEditor.removeSprite(selected.get() ); } } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, SELECTING_SPRITES { @Override public void defaultClickAction(LevelDrawingCanvas editor) { } @Override public void defaultDragAction(LevelDrawingCanvas editor) { } }, PLACING_BONZO { @Override public void defaultClickAction(LevelDrawingCanvas editor) { editor.setBonzo(editor.mousePosition.x(), editor.mousePosition.y() ); } @Override public void defaultDragAction(LevelDrawingCanvas editor) { defaultClickAction(editor); } }, NO_WORLD_LOADED { @Override public void defaultClickAction(LevelDrawingCanvas editor) { } @Override public void defaultDragAction(LevelDrawingCanvas editor) { } }; } /** * Returns the visible screen editor. Note that this may return {@code null} if no world is loaded! * * @return * the current screen editor, or {@code null} if no world is loaded * */ public LevelScreenEditor getVisibleScreenEditor() { return this.currentScreenEditor; } /** * * Removes the selected sprite from the level and replaces it with the information taken from the * editor sprite dialog model * <p/> * The new sprite replaces the removed sprite, so the list of sprites is not re-ordered by this operation. * This is important as sprite chooser dialogs, if the sprite is otherwise identical, the only other info * the user has in choosing a sprite is where it appears in the dialog. * * @param sprite * the old sprite to replace * * @param model * the model for the new sprite * */ private void changeSpriteFromDialogModel(Sprite sprite, SpritePropertiesModel model) { Sprite newSprite = Sprite.newSprite( model.getSpriteId(), model.getSpriteStartingLocation(), model.getSpriteBoundingBox(), model.getSpriteVelocity(), model.getAnimationType(), model.getAnimationSpeed(), model.getSpriteType(), model.getForceDirection(), model.getTwoWayFacing(), currentWorldEditor.getWorldResource() ); newSprite.setVisible(sprite.isVisible() ); currentScreenEditor.replaceSprite(sprite, newSprite); } // Point2D of the mouse position private Point2D mousePosition; // Information about what clicking something will do private Goodie.Type currentGoodieType; private int currentSpriteId; // May be null, but should never be accessed until Editor State goes to placing templates, which // requires a non-null template to even get to that state. private Template currentTemplate; // Toggled by user whilst drawing. Up to client classes to decide how to enable/disable this property private boolean drawingCoordinates; private final Function<World, Void> worldLoaded; // THESE MAY BE NULL! private LevelScreenEditor currentScreenEditor; private WorldEditor currentWorldEditor; private MapEditor currentMapEditor; private Timer editorFakeGameTimer; private BufferedImage indicatorImage = null; private EditorState currentState; }